/*
 *	This file is part of Transdroid <http://www.transdroid.org>
 *
 *	Transdroid is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *
 *	Transdroid is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with Transdroid.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package org.transdroid.daemon.adapters.kTorrent;

import com.android.internal.http.multipart.FilePart;
import com.android.internal.http.multipart.MultipartEntity;
import com.android.internal.http.multipart.Part;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ProtocolException;
import org.apache.http.client.RedirectHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.transdroid.core.gui.log.Log;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByMagnetUrlTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.util.HttpHelper;

import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;


/**
 * An adapter that allows for easy access to Ktorrent's web interface. Communication is handled via HTTP GET requests
 * and XML responses.
 *
 * @author erickok
 */
public class KTorrentAdapter implements IDaemonAdapter {

    private static final String LOG_NAME = "Ktorrent daemon";

    private static final String RPC_URL_CHALLENGE = "/login/challenge.xml";
    private static final String RPC_URL_LOGIN = "/login?page=interface.html";
    private static final String RPC_URL_LOGIN_USER = "username";
    private static final String RPC_URL_LOGIN_PASS = "password";
    private static final String RPC_URL_LOGIN_CHAL = "challenge";
    private static final String RPC_URL_STATS = "/data/torrents.xml?L10n=no";
    private static final String RPC_URL_ACTION = "/action?";
    private static final String RPC_URL_UPLOAD = "/torrent/load?page=interface.html";
    private static final String RPC_URL_FILES = "/data/torrent/files.xml?torrent=";
    //private static final String RPC_COOKIE_NAME = "KT_SESSID";
    private static final String RPC_SUCCESS = "<result>OK</result>";
    static private int retries = 0;
    private DaemonSettings settings;
    private DefaultHttpClient httpclient;

    /**
     * Initialises an adapter that provides operations to the Ktorrent web interface
     */
    public KTorrentAdapter(DaemonSettings settings) {
        this.settings = settings;
    }

    /**
     * Calculate the SHA1 hash of a password/challenge string to use with the login requests.
     *
     * @param passkey A concatenation of the challenge string and plain text password
     * @return A hex-formatted SHA1-hashed string of the challenge and password strings
     */
    public static String sha1Pass(String passkey) {
        try {
            MessageDigest m = MessageDigest.getInstance("SHA1");
            byte[] data = passkey.getBytes();
            m.update(data, 0, data.length);
            BigInteger i = new BigInteger(1, m.digest());
            return String.format("%1$040X", i).toLowerCase();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public DaemonTaskResult executeTask(Log log, DaemonTask task) {

        try {
            switch (task.getMethod()) {
                case Retrieve:

                    // Request all torrents from server
                    return new RetrieveTaskSuccessResult((RetrieveTask) task, makeStatsRequest(log), null);

                case GetFileList:

                    // Request file listing for a torrent
                    return new GetFileListTaskSuccessResult((GetFileListTask) task,
                            makeFileListRequest(log, task.getTargetTorrent()));

                case AddByFile:

                    // Add a torrent to the server by sending the contents of a local .torrent file
                    String file = ((AddByFileTask) task).getFile();
                    makeFileUploadRequest(log, file);
                    return null;

                case AddByUrl:

                    // Request to add a torrent by URL
                    String url = ((AddByUrlTask) task).getUrl();
                    makeActionRequest(log, "load_torrent=" + url);
                    return new DaemonTaskSuccessResult(task);

                case AddByMagnetUrl:

                    // Request to add a magnet link by URL
                    String magnet = ((AddByMagnetUrlTask) task).getUrl();
                    makeActionRequest(log, "load_torrent=" + magnet);
                    return new DaemonTaskSuccessResult(task);

                case Remove:

                    // Remove a torrent
                    // Note that removing with data is not supported
                    makeActionRequest(log, "remove=" + task.getTargetTorrent().getUniqueID());
                    return new DaemonTaskSuccessResult(task);

                case Pause:

                    // Pause a torrent
                    makeActionRequest(log, "stop=" + task.getTargetTorrent().getUniqueID());
                    return new DaemonTaskSuccessResult(task);

                case PauseAll:

                    // Pause all torrents
                    makeActionRequest(log, "stopall=true");
                    return new DaemonTaskSuccessResult(task);

                case Resume:

                    // Resume a torrent
                    makeActionRequest(log, "start=" + task.getTargetTorrent().getUniqueID());
                    return new DaemonTaskSuccessResult(task);

                case ResumeAll:

                    // Resume all torrents
                    makeActionRequest(log, "startall=true");
                    return new DaemonTaskSuccessResult(task);

                case SetFilePriorities:

                    // Set the priorities of the files of some torrent
                    SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
                    String act = "file_np=" + task.getTargetTorrent().getUniqueID() + "-";
                    switch (prioTask.getNewPriority()) {
                        case Off:
                            act = "file_stop=" + task.getTargetTorrent().getUniqueID() + "-";
                            break;
                        case Low:
                        case Normal:
                            act = "file_lp=" + task.getTargetTorrent().getUniqueID() + "-";
                            break;
                        case High:
                            act = "file_hp=" + task.getTargetTorrent().getUniqueID() + "-";
                            break;
                    }
                    // It seems KTorrent's web UI does not allow for setting all priorities in one request :(
                    for (TorrentFile forFile : prioTask.getForFiles()) {
                        makeActionRequest(log, act + forFile.getKey());
                    }
                    return new DaemonTaskSuccessResult(task);

                case SetTransferRates:

                    // Request to set the maximum transfer rates
                    // TODO: Implement this?
                    return null;

                default:
                    return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported,
                            task.getMethod() + " is not supported by " + getType()));
            }
        } catch (LoggedOutException e) {

            // Invalidate our session
            httpclient = null;
            if (retries < 2) {
                retries++;
                // Retry
                log.d(LOG_NAME, "We were logged out without knowing: retry");
                return executeTask(log, task);
            } else {
                // Never retry more than twice; in this case just return a task failure
                return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ConnectionError,
                        "Retried " + retries + " already, so we stopped now"));
            }

        } catch (DaemonException e) {

            // Invalidate our session
            httpclient = null;
            // Return the task failure
            return new DaemonTaskFailureResult(task, e);

        }
    }

    private List<Torrent> makeStatsRequest(Log log) throws DaemonException, LoggedOutException {

        try {

            // Initialise the HTTP client
            initialise();
            makeLoginRequest(log);

            // Make request
            HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_STATS);
            HttpResponse response = httpclient.execute(httpget);

            // Read XML response
            InputStream instream = response.getEntity().getContent();
            List<Torrent> torrents = StatsParser.parse(new InputStreamReader(instream), settings.getDownloadDir(),
                    settings.getOS().getPathSeperator());
            instream.close();
            return torrents;

        } catch (LoggedOutException e) {
            throw e;
        } catch (DaemonException e) {
            log.d(LOG_NAME, "Parsing error: " + e.toString());
            throw e;
        } catch (Exception e) {
            log.d(LOG_NAME, "Error: " + e.toString());
            throw new DaemonException(ExceptionType.ConnectionError, e.toString());
        }

    }

    private List<TorrentFile> makeFileListRequest(Log log, Torrent torrent) throws DaemonException, LoggedOutException {

        try {

            // Initialise the HTTP client
            initialise();
            makeLoginRequest(log);

            // Make request
            HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_FILES + torrent.getUniqueID());
            HttpResponse response = httpclient.execute(httpget);

            // Read XML response
            InputStream instream = response.getEntity().getContent();
            List<TorrentFile> files = FileListParser.parse(new InputStreamReader(instream), torrent.getLocationDir());
            instream.close();

            // If the files list is empty, it means that this is a single-file torrent
            // We can mimic this single file form the torrent statistics itself
            files.add(new TorrentFile("" + 0, torrent.getName(), torrent.getName(),
                    torrent.getLocationDir() + torrent.getName(), torrent.getTotalSize(), torrent.getDownloadedEver(),
                    Priority.Normal));

            return files;

        } catch (LoggedOutException e) {
            throw e;
        } catch (DaemonException e) {
            log.d(LOG_NAME, "Parsing error: " + e.toString());
            throw e;
        } catch (Exception e) {
            log.d(LOG_NAME, "Error: " + e.toString());
            throw new DaemonException(ExceptionType.ConnectionError, e.toString());
        }

    }

    private void makeLoginRequest(Log log) throws DaemonException {

        try {

            // Make challenge request
            HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_CHALLENGE);
            HttpResponse response = httpclient.execute(httpget);
            InputStream instream = response.getEntity().getContent();
            String challengeString = HttpHelper.convertStreamToString(instream).replaceAll("<.*?>", "").trim();
            instream.close();
            // Challenge string should be something like TncpX3TB8uZ0h8eqztZ6
            if (challengeString.length() != 20) {
                throw new DaemonException(ExceptionType.UnexpectedResponse, "No (valid) challenge string received");
            }

            // Make login request
            HttpPost httppost2 = new HttpPost(buildWebUIUrl() + RPC_URL_LOGIN);
            List<NameValuePair> params = new ArrayList<>(3);
            params.add(new BasicNameValuePair(RPC_URL_LOGIN_USER, settings.getUsername()));
            params.add(new BasicNameValuePair(RPC_URL_LOGIN_PASS,
                    "")); // Password is send (as SHA1 hex) in the challenge field
            params.add(new BasicNameValuePair(RPC_URL_LOGIN_CHAL, sha1Pass(challengeString +
                    settings.getPassword()))); // Make a SHA1 encrypted hex-formated string of the challenge code and password
            httppost2.setEntity(new UrlEncodedFormEntity(params));
            // This sets the authentication cookie
            httpclient.execute(httppost2);
			/*InputStream instream2 = response2.getEntity().getContent();
			String result2 = HttpHelper.ConvertStreamToString(instream2);
			instream2.close();*/

            // Successfully logged in; we may retry later if needed
            retries = 0;

        } catch (DaemonException e) {
            log.d(LOG_NAME, "Login error: " + e.toString());
            throw e;
        } catch (Exception e) {
            log.d(LOG_NAME, "Error during login: " + e.toString());
            throw new DaemonException(ExceptionType.ConnectionError, e.toString());
        }

    }

    private boolean makeActionRequest(Log log, String action) throws DaemonException, LoggedOutException {

        try {

            // Initialise the HTTP client
            initialise();
            makeLoginRequest(log);

            // Make request
            HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_ACTION + action);
            HttpResponse response = httpclient.execute(httpget);

            // Read response (a successful action always returned '1')
            InputStream instream = response.getEntity().getContent();
            String result = HttpHelper.convertStreamToString(instream);
            instream.close();
            if (result.contains(RPC_SUCCESS)) {
                return true;
            } else if (result.contains("KTorrent  WebInterface - Login")) {
                // Apparently we were returned an HTML page instead of the expected XML
                // This happens in particular when we were logged out (because somebody else logged into KTorrent's web interface)
                throw new LoggedOutException();
            } else {
                throw new DaemonException(ExceptionType.UnexpectedResponse, "Action response was not OK but " + result);
            }

        } catch (LoggedOutException e) {
            throw e;
        } catch (DaemonException e) {
            log.d(LOG_NAME, action + " request error: " + e.toString());
            throw e;
        } catch (Exception e) {
            log.d(LOG_NAME, "Error: " + e.toString());
            throw new DaemonException(ExceptionType.ConnectionError, e.toString());
        }

    }

    private boolean makeFileUploadRequest(Log log, String target) throws DaemonException, LoggedOutException {

        try {

            // Initialise the HTTP client
            initialise();
            makeLoginRequest(log);

            // Make request
            HttpPost httppost = new HttpPost(buildWebUIUrl() + RPC_URL_UPLOAD);
            File upload = new File(URI.create(target));
            Part[] parts = {new FilePart("load_torrent", upload)};
            httppost.setEntity(new MultipartEntity(parts, httppost.getParams()));
            // Make sure we are not automatically redirected
            RedirectHandler handler = new RedirectHandler() {
                @Override
                public boolean isRedirectRequested(HttpResponse response, HttpContext context) {
                    return false;
                }

                @Override
                public URI getLocationURI(HttpResponse response, HttpContext context) throws ProtocolException {
                    return null;
                }
            };
            httpclient.setRedirectHandler(handler);
            HttpResponse response = httpclient.execute(httppost);

            // Read response (a successful action always returned '1')
            InputStream instream = response.getEntity().getContent();
            String result = HttpHelper.convertStreamToString(instream);
            instream.close();
            if (result.equals("")) {
                return true;
            } else if (result.contains("KTorrent  WebInterface - Login")) {
                // Apparently we were returned an HTML page instead of the expected XML
                // This happens in particular when we were logged out (because somebody else logged into KTorrent's web interface)
                throw new LoggedOutException();
            } else {
                throw new DaemonException(ExceptionType.UnexpectedResponse, "Action response was not 1 but " + result);
            }

        } catch (LoggedOutException e) {
            throw e;
        } catch (DaemonException e) {
            log.d(LOG_NAME, "File upload error: " + e.toString());
            throw e;
        } catch (Exception e) {
            log.d(LOG_NAME, "Error: " + e.toString());
            throw new DaemonException(ExceptionType.ConnectionError, e.toString());
        }

    }

    /**
     * Indicates if we were already successfully authenticated
     * @return True if the proper authentication cookie was already loaded
     */
	/*private boolean authenticated() {
		// We should have a Ktorrent cookie in the httpclient when we are authenticated
		for(Cookie cookie : httpclient.getCookieStore().getCookies()) {
			if (cookie.getName().equals(RPC_COOKIE_NAME)) {
				return true;
			}
		}
		return false;
	}*/

    /**
     * Instantiates an HTTP client that can be used for all Ktorrent requests.
     *
     * @throws DaemonException Thrown on settings error
     */
    private void initialise() throws DaemonException {

        if (httpclient != null) {
            httpclient = null;
        }
        httpclient = HttpHelper.createStandardHttpClient(settings, false);
    }

    /**
     * Build the base URL for a Ktorrent web site request from the user settings.
     *
     * @return The base URL of for a request, i.e. http://localhost:8080
     */
    private String buildWebUIUrl() {
        return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort();
    }

    @Override
    public Daemon getType() {
        return settings.getType();
    }

    @Override
    public DaemonSettings getSettings() {
        return this.settings;
    }

}
